Item 3: 尽可能使用 const

const 指定一个“不该被改动”的对象,编译器会强制实施这项约束。

const 与指针

char greeting[] = "Hello";
char* p = greeting;	//@ non-const data,non-const pointer
const char* p = greeting;	//@ non-const pointer,const data
char* const p = greeting;	//@ const pointer,non-const data
const char* const p = greeting; //@ const pointer,const data
  • const 出现在 * 左边,则指针指向的内容是 const。
  • const 出现在 * 右边,则指针本身是 const。
  • const 出现在 * 两边,两者都是 const。

C++ Primer 5th :弄清楚声明的含义:从右向左阅读,离变量名最近的对变量有直接影响,其余部分确定对象的类型

当指针指向的内容是常量时,将 const 放在类型前和放在类型后是没有区别的:

//@ 等价的形式
void f1(const Widget *pw);	
void f1(Widget const *pw);	

变与不变

当指针指向的内容是常量时,表示无法通过指针修改变量的值,但是可以通过其它方式修改指针指向变量的值:

int a = 1;
const int *p = &a;
cout << *p << endl;	//@ 1
*p = 2;	//@ error, data is const
a = 2;
cout << *p << endl;	//@ 2

指针本身是常量,表示指针表示的地址是固定的,但是其指向的内容是可以改变的:

int a = 1, b = 2;
int* const p = &a;
cout << *p << endl;	//@ 1
p = &b;	//@ error, pointer is const
*p = b;
cout << *p << endl;	//@ 2

const 与迭代器

STL 迭代器以指针为原型,所以 iterator 的作用就像个 T* 指针。声明一个 iterator 为 const 就类似于声明一个 pointer 为 const(也就是说,声明一个 T* const pointer):

std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin();
*iter = 10;	//@ ok,change what the iterator point to
iter++;		//@ error,iter is const

std::vector<int>::const_iterator cIter =  vec.begin();
*cIter = 10;	//@ error,*cIter is const
++cIter;		//@ ok,change cIter

const 与函数

const 可以用在函数返回值,函数的个别参数,对于成员函数,还可以用于整个函数。

函数返回 const value

令函数返回一个常量值,往往可以在不放弃安全性和高效性的前提下降低因客户错误而造成的意外。

class Rational{...};
const Rational operator*(const Rational& lhs,const Rational& rhs);

Rational a,b,c;
...
(a * b) = c;	//为两个数的乘积赋值,将返回值声明为const 可以避免此问题

const 成员函数

  • 声明 const 成员函数是为了确认哪些方法可以通过常量对象来访问
  • 使 class 接口比较容易被理解,容易知道哪个函数可以改动对象而哪个函数不行
  • 常量对象只能调用常量方法, 非常量对象优先调用非常量方法,如不存在会调用同名常量方法
  • 常量成员函数也可以在类声明外定义,但声明和定义都需要指定 const 关键字
  • 成员方法添加常量限定符属于函数重载,这是C++的一个重要特性
class TextBlock {
public:
  ...  
  //@ operator[] for const objects
  const char& operator[](std::size_t position) const  
  { return text[position]; }                          

  //@ operator[] for non-const objects
  char& operator[](std::size_t position)           
  { return text[position]; }                          

private:
   std::string text;
};

//@ 使用
TextBlock tb("Hello");
std::cout << tb[0];	//@ calls non-const TextBlock::operator[]
                                       
const TextBlock ctb("World");
std::cout << ctb[0];  //@ calls const TextBlock::operator[]

真实程序中 const 对象大多用于 passed by pointer-to-const passed by reference-to-const

void print(const TextBlock& ctb)       // in this function, ctb is const
{
  std::cout << ctb[0];                 // calls const TextBlock::operator[]
  ...
}
//@ 对 const 和 non-const 的 TextBlocks 做不同的操作
std::cout << tb[0];   //@ fine — reading a non-const TextBlock
tb[0] = 'x';         //@ fine — writing a non-const TextBlock
std::cout << ctb[0]; //@ fine — reading a const TextBlock
ctb[0] = 'x';       //@ error! — writing a const TextBlock

错误只与 operator[] 返回类型有关,而 operator[] 调用动作自身没问题。

如果 non-const 版本的 operator[] 返回一个char而不是一个char引用,则下面语句将无法编译通过:

tb[0] = 'x';	//返回一个右值,企图为一个右值赋值

bitwise constness 和 logical constness

(比特常量和逻辑常量)

比特常量:成员函数只有在不更改对象内的任何非静态成员变量,那该函数是const的,即不更改对象内任何一个 bit。比特常量是C++ 对常量性(constness)的定义。

不幸的是,许多成员函数虽然不具备 const 性质,却能通过 bitwise测试:一个更改了“指针所指物”的成员函数不能算const,但如果只有指针(而非其所指物)隶属于对象,则称此函数为 bitwise const 不会引发编译器异议。

class CTextBlock {
public:
  ...
  char& operator[](std::size_t position) const   // inappropriate (but bitwise
  { return pText[position]; }                    // const) declaration of
                                                 // operator[]
private:
  char *pText;
};

看看 operator[] 的实现,它并没有使用任何手段改变 pText。结果,编译器愉快地生成了 operator[] 的代码,因为毕竟对所有编译器而言,它都是 bitwise const 的,但是我们看看会发生什么:

const CTextBlock cctb("Hello");   //@ declare constant object
char *pc = &cctb[0];  //@ call the const operator[] to get a pointer to cctb's data
*pc = 'J'; //@ cctb now has the value "Jello"

这里确实出了问题,你创建一个常量对象并设以某值,然后你只是用它调用了 const 成员函数,但是你还是改变了它的值!

这种情况导出所谓的 logical constness(逻辑常量):一个 const 成员函数可以修改它所处理的对象内的某些 bits,但只有在客户端侦测不到的情况下才得如此。

const 成员函数修改对象内容对对象而言虽然可以接受,但编译器不同意,此时需要 mutable 限定符:mutable 释放掉non-static 成员变量的 bitwise constness 约束:

class CTextBlock {
public:
  ...
  std::size_t length() const;
private:
  char *pText;
  mutable std::size_t textLength;	//这些成员变量可能总是会被更改
  mutable bool lengthIsValid;		//即使在const 成员函数内
};    

std::size_t CTextBlock::length() const
{
  if (!lengthIsValid) {
    textLength = std::strlen(pText);      //now fine
    lengthIsValid = true;                 //also fine
  }

  return textLength;
}

在 const 和 non-const 成员函数中避免重复

class TextBlock {
public:
  ...
  const char& operator[](std::size_t position) const
  {
    ...	//边界检验(bounds checking)
    ... //日志访问数据(log access data)
    ... //检验数据完整性(verify data integrity)
    return text[position];
  }

  char& operator[](std::size_t position)
  {
    ...	//边界检验(bounds checking)
    ... //日志访问数据(log access data)
    ... //检验数据完整性(verify data integrity)
    return text[position];
  }
...
private:
    std::string text;
};

代码重复伴随着编译时间、维护、代码膨胀等问题。

真正需要的是一次 operator[] 功能实现,然后使用它两次,即必须令其中一个调用另一个。

运用 const 成员函数实现了其 non-const 成员函数:

class TextBlock {
public:
  ...
  const char& operator[](std::size_t position) const     // same as before
  {
    ...
    return text[position];
  }

  char& operator[](std::size_t position)         // now just calls const op[]
  {
    return const_cast<char&>(
        static_cast<const TextBlock&>(*this)[position]);
  }
...
};
  • *this 的类型是 TextBlock ,先把它强制隐式转换为 const TextBlock,这样才能调用常量方法。
  • 调用 operator[](size_t) const ,得到的返回值类型为 const char&。
  • 把返回值去掉 const 属性,得到类型为 char& 的返回值。

反之,运用 non-const 成员函数实现了其 const 成员函数是错误的,const成员函数承诺绝不改变其对象的逻辑状态,而 non-const 成员函数没有这般承诺。

non-const成员函数本身就可以对对象做任何动作,因此以static_cast处理*this并不存在风险。

总结

  • 将某些东西声明为 const 可帮助编译器侦测出错误用法
  • const 与指针:
    • const 在前表示指针指向的内容是常量
    • *号在前表示指针本身是常量
  • const 与迭代器:
    • const 修饰迭代器时表示迭代器本身是常量
    • 迭代器指向的内容是常量时应该使用 const_iterator
  • const 与函数
    • 函数返回值为 const 可以避免一些意外赋值的情况发生
    • 尽可能的将函数参数声明为 const
    • 常量对象只能调用常量方法, 非常量对象优先调用非常量方法,如不存在会调用同名常量方法
    • 如果一个方法不改变对象的任何非静态变量,那么该方法是常量方法
    • mutable 限定符对于即使是 const 的对象也可以做修改
    • 当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可以避免代码重复
  • 编译器强制实施 bitwise constness,但编写程序时应该使用“概念上的常量性”